﻿using System;
using System.Globalization;
using System.IO.Ports;
using System.Threading;

namespace UPSAgent
{
    public class Apc
    {
        #region "Общение" с ИБП

        private void WriteCommand(string command)
        {
            _comPort.Write(command[0].ToString());
            for (var i = 1; i < command.Length; i++)
            {
                Thread.Sleep(250);
                _comPort.Write(command[i].ToString());
            }
        }

        private string ReadCommand()
        {
            string result;
            try
            {
                result = _comPort.ReadLine();
            }
            catch (TimeoutException)
            {
                result = _comPort.ReadExisting();
            }
            return result;
        }

        public bool RunCommand(string command, string validAnswer = "OK", bool doubleCommand = false)
        {
            if (doubleCommand)
            {
                WriteCommand(command);
                Thread.Sleep(1600);
                if (String.IsNullOrEmpty(validAnswer))
                {
                    WriteCommand(command);
                    return true;
                }
                return GetString(command) == validAnswer;
            }
            if (String.IsNullOrEmpty(validAnswer))
            {
                WriteCommand(command);
                return true;
            }
            return GetString(command) == validAnswer;
        }

        public string GetString(string command)
        {
            _commandRunning = true;
            WriteCommand(command);
            var result = ReadCommand();
            _commandRunning = false;
            while (!String.IsNullOrEmpty(result) && Enum.IsDefined(typeof (UpsEventTypes), (int) result[0]))
            {
                ParseEvent(result[0]);
                result = result.Remove(0, 1);
            }
            return result;
        }

        public decimal GetDecimal(string command)
        {
            var answer = GetString(command);
            decimal result;
            return Decimal.TryParse(answer, NumberStyles.Number, _numberFormatInfo, out result) ? result : 0;
        }

        private int GetHexInteger(string command)
        {
            var answer = GetString(command);
            int result;
            return Int32.TryParse(answer, NumberStyles.HexNumber, _numberFormatInfo, out result) ? result : 0;
        }

        private int GetInteger(string command)
        {
            var answer = GetString(command);
            int result;
            return Int32.TryParse(answer, NumberStyles.Number, _numberFormatInfo, out result) ? result : 0;
        }

        private DateTime GetDateTime(string command)
        {
            DateTime result;
            return DateTime.TryParse(GetString(command), CultureInfo.CreateSpecificCulture("en-US"), DateTimeStyles.None,
                                     out result)
                       ? result
                       : DateTime.MinValue;
        }

        #endregion

        #region Информационные сообщения от ИБП

        #region Delegates

        public delegate void UpsEventHandler(object sender, UpcEventArgs args);

        #endregion

        public enum UpsEventTypes
        {
            LineFail = '!',
            ReturnFromLineFail = '$',
            LowBattery = '%',
            ReturnFromLowBattery = '+',
            AbnormalCondition = '?',
            ReturnFromAbnormalCondition = '=',
            AboutToTurnOff = '*',
            ReplaceBattery = '#',
            CheckAlarmRegisterForFault = '&',
            VariableChangeInEeprom = '|'
        }

        private void ParseEvent(int eventChar)
        {
            if (!Enum.IsDefined(typeof (UpsEventTypes), eventChar)) return;
            if (UpsEvent != null)
            {
                UpsEvent(this, new UpcEventArgs((UpsEventTypes) eventChar));
            }
        }

        public event UpsEventHandler UpsEvent;

        public class UpcEventArgs : EventArgs
        {
            public UpcEventArgs(UpsEventTypes eventType)
            {
                EventType = eventType;
            }

            public UpsEventTypes EventType { get; private set; }
        }

        #endregion

        #region Communication

        private bool _commandRunning;

        private void ComPortDataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            if (_commandRunning) return;
            while (_comPort.BytesToRead > 0)
            {
                ParseEvent(_comPort.ReadChar());
            }
        }

        #endregion

        #region Nationals enum

        public enum Nationals
        {
            Domestic,
            International,
            Military,
            Japan,
            Unknown
        }

        #endregion

        private readonly SerialPort _comPort;
        private readonly NumberFormatInfo _numberFormatInfo;

        public Apc(String portName)
        {
            _numberFormatInfo = new NumberFormatInfo
                                    {
                                        NumberDecimalSeparator = ".",
                                    };
            _comPort = new SerialPort
                           {
                               BaudRate = 2400,
                               DataBits = 8,
                               NewLine = "\r\n",
                               Parity = Parity.None,
                               StopBits = StopBits.One,
                               PortName = portName,
                               DiscardNull = true,
                               ReadTimeout = 5000
                           };
            _comPort.Open();
            _comPort.DataReceived += ComPortDataReceived;
            if (!RunCommand("Y", "SM"))
                throw new Exception("Неправильный ответ от устройства. Вероятно несовместимое устройство.");
            SetValues();
        }

        public string CommercialName { get; private set; }
        public int Generation { get; private set; }
        public Nationals National { get; private set; }

        private void SetValues()
        {
            var ver = GetString("V");
            switch (ver[0])
            {
                case '0':
                    CommercialName = "Matrix 3000";
                    break;
                case '5':
                    CommercialName = "Matrix 5000";
                    break;
                case '2':
                    CommercialName = "Smart-UPS 250";
                    break;
                case '3':
                    CommercialName = "Smart-UPS 400";
                    break;
                case '4':
                    CommercialName = "Smart-UPS 400";
                    break;
                case '6':
                    CommercialName = "Smart-UPS 600";
                    break;
                case '7':
                    CommercialName = "Smart-UPS 900";
                    break;
                case '8':
                    CommercialName = "Smart-UPS 1250";
                    break;
                case '9':
                    CommercialName = "Smart-UPS 2000";
                    break;
                case 'A':
                    CommercialName = "Smart-UPS 1400";
                    break;
                case 'B':
                    CommercialName = "Smart-UPS 1000";
                    break;
                case 'C':
                    CommercialName = "Smart-UPS 650";
                    break;
                case 'D':
                    CommercialName = "Smart-UPS 420";
                    break;
                case 'E':
                    CommercialName = "Smart-UPS 280";
                    break;
                case 'F':
                    CommercialName = "Smart-UPS 450";
                    break;
                case 'G':
                    CommercialName = "Smart-UPS 700";
                    break;
                case 'H':
                    CommercialName = "Smart-UPS 700XL";
                    break;
                case 'I':
                    CommercialName = "Smart-UPS 1000";
                    break;
                case 'J':
                    CommercialName = "Smart-UPS 1000XL";
                    break;
                case 'K':
                    CommercialName = "Smart-UPS 1400";
                    break;
                case 'L':
                    CommercialName = "Smart-UPS 1400XL";
                    break;
                case 'M':
                    CommercialName = "Smart-UPS 2200";
                    break;
                case 'N':
                    CommercialName = "Smart-UPS 2200XL";
                    break;
                case 'O':
                    CommercialName = "Smart-UPS 3000";
                    break;
                case 'P':
                    CommercialName = "Smart-UPS 5000";
                    break;
                case 'Q':
                    CommercialName = "Back-UPS";
                    break;
                default:
                    CommercialName = "Unknown";
                    break;
            }
            switch (ver[1])
            {
                case 'W':
                    Generation = 3;
                    break;
                case 'Q':
                    Generation = 2;
                    break;
                case 'T':
                    Generation = 1;
                    break;
                case 'U':
                    Generation = 0;
                    break;
                default:
                    Generation = -1;
                    break;
            }
            switch (ver[2])
            {
                case 'D':
                    National = Nationals.Domestic;
                    break;
                case 'I':
                    National = Nationals.International;
                    break;
                case 'M':
                    National = Nationals.Military;
                    break;
                case 'J':
                    National = Nationals.Japan;
                    break;
                default:
                    National = Nationals.Unknown;
                    break;
            }

            LoadConstants();
        }

        #region Чтение и запись

        public AutomaticSelftestIntervals AutomaticSelftestInterval
        {
            get
            {
                var r = GetString("E");
                switch (r)
                {
                    case "OFF":
                        return AutomaticSelftestIntervals.Off;
                    case "ON":
                        return AutomaticSelftestIntervals.On;
                    case "168":
                        return AutomaticSelftestIntervals.Days7;
                    case "336":
                        return AutomaticSelftestIntervals.Days14;
                    default:
                        return AutomaticSelftestIntervals.Unknown;
                }
            }
        }

        public string UpsLocalId
        {
            get { return GetString("c"); }
            set
            {
                if (value.Length > 8) value = value.Remove(8);
                else if (value.Length < 8) value = value.PadRight(8);
                RunCommand("c+" + value);
            }
        }

        public ReturnThresholds ReturnThreshold
        {
            get { return (ReturnThresholds) GetInteger("e"); }
        }

        public int NominalBatteryVoltage
        {
            get { return GetInteger("g"); }
        }

        public AlarmDelays AlarmDelay
        {
            get
            {
                var r = GetString("k");
                switch (r)
                {
                    case "0":
                        return AlarmDelays.Seconds5;
                    case "T":
                        return AlarmDelays.Seconds30;
                    case "L":
                        return AlarmDelays.LowBatteryOnly;
                    case "N":
                        return AlarmDelays.Disabled;
                    default:
                        return AlarmDelays.Unknown;
                }
            }
        }

        public int LowTransferVoltage
        {
            get { return GetInteger("l"); }
        }

        public int OnBatteryVoltage
        {
            get { return GetInteger("o"); }
        }

        public int ShutdownGraceDelay
        {
            get { return GetInteger("p"); }
        }

        public int LowBatteryWarning
        {
            get { return GetInteger("q"); }
        }

        public int WakeupDelay
        {
            get { return GetInteger("r"); }
        }

        public Sensitivities Sensitivity
        {
            get
            {
                var r = GetString("s");
                switch (r)
                {
                    case "H":
                        return Sensitivities.Highest;
                    case "M":
                        return Sensitivities.Medium;
                    case "L":
                        return Sensitivities.Lowest;
                    case "A":
                        return Sensitivities.Autoadjust;
                    default:
                        return Sensitivities.Unknown;
                }
            }
        }

        public int UpperTransferVoltage
        {
            get { return GetInteger("u"); }
        }

        public DateTime LastBatteryChange
        {
            get { return GetDateTime("x"); }
        }

        #endregion

        #region Константы

        public string Model { get; private set; }
        public string Capability { get; private set; }
        public string OldFirmwareRevision { get; private set; }
        public string ProtocolInfo { get; private set; }
        public string FirmwareRevision { get; private set; }
        public DateTime ManufacturingDate { get; private set; }
        public string SerialNumber { get; private set; }
        public string CopyrightNotice { get; private set; }

        public void LoadConstants()
        {
            Model = GetString("\x01");
            Capability = GetString("\x1a");
            OldFirmwareRevision = GetString("V");
            ProtocolInfo = GetString("a");
            FirmwareRevision = GetString("b");
            ManufacturingDate = GetDateTime("m");
            SerialNumber = GetString("n");
            CopyrightNotice = GetString("y");
        }

        #endregion

        #region Регистры

        public int Register0
        {
            get { return GetHexInteger("0"); }
        }

        public Register1Flags Register1
        {
            get { return (Register1Flags) GetHexInteger("~"); }
        }

        public Register2Flags Register2
        {
            get { return (Register2Flags) GetHexInteger("'"); }
        }

        public Register3Flags Register3
        {
            get { return (Register3Flags) GetHexInteger("8"); }
        }

        public int Register4
        {
            get { return GetHexInteger("4"); }
        }

        public int Register5
        {
            get { return GetHexInteger("5"); }
        }

        public int Register6
        {
            get { return GetHexInteger("6"); }
        }

        #endregion

        #region Команды

        public void TurnOnUps()
        {
            RunCommand("\x0e", null, true);
        }

        public bool FrontPanelTest()
        {
            return RunCommand("A");
        }

        public void RuntimeCalibration()
        {
            RunCommand("D", null);
        }

        public bool ShutdownWithGracePeriod()
        {
            return RunCommand("K", doubleCommand: true);
        }

        public bool TurnDumb()
        {
            return RunCommand("R", "BYE");
        }

        public bool SoftShutdown()
        {
            return RunCommand("S");
        }

        public void SimulatePowerFailure()
        {
            RunCommand("U", null);
        }

        public bool SelfTest()
        {
            return RunCommand("W");
        }

        public bool EnterSmartMode()
        {
            return RunCommand("Y", "SM");
        }

        public void ShutdownImmediately()
        {
            RunCommand("Z", null, true);
        }

        public bool ResetToFactorySettings()
        {
            return RunCommand("z", "CLEAR");
        }

        public bool ShutdownAndReturn(int time)
        {
            return RunCommand("@" + time.ToString("000"));
        }

        public bool AbortShutdown()
        {
            return RunCommand("\x7f");
        }

        #endregion

        #region Только чтение (переменные)

        public decimal BatteryVoltage
        {
            get { return GetDecimal("B"); }
        }

        public decimal LineFrequency
        {
            get { return GetDecimal("F"); }
        }

        public CausesOfTransfer CauseOfTransfer
        {
            get
            {
                var r = GetString("G");
                switch (r)
                {
                    case "R":
                        return CausesOfTransfer.UnacceptableUtilityVoltageRateOfChange;
                    case "H":
                        return CausesOfTransfer.HighUtilityVoltage;
                    case "L":
                        return CausesOfTransfer.LowUtilityVoltage;
                    case "T":
                        return CausesOfTransfer.LineVoltageNotchOrSpike;
                    case "O":
                        return CausesOfTransfer.NoTransfersYetSinceTurnon;
                    case "S":
                        return CausesOfTransfer.TransferDueToUcommandOrActivationOfUpsTest;
                    default:
                        return CausesOfTransfer.Unknown;
                }
            }
        }

        public decimal InputLineVoltage
        {
            get { return GetDecimal("L"); }
        }

        public decimal MaximumLineVoltage
        {
            get { return GetDecimal("M"); }
        }

        public decimal MinimumLineVoltage
        {
            get { return GetDecimal("N"); }
        }

        public decimal OutputVoltage
        {
            get { return GetDecimal("O"); }
        }

        public decimal PowerLoad
        {
            get { return GetDecimal("P"); }
        }

        public StatusFlags Status
        {
            get { return (StatusFlags) GetHexInteger("Q"); }
        }

        public SelfTestResults SelfTestResult
        {
            get
            {
                var r = GetString("X");
                switch (r)
                {
                    case "OK":
                        return SelfTestResults.GoodBattery;
                    case "BT":
                        return SelfTestResults.FailedDueToInsufficientCapacity;
                    case "NG":
                        return SelfTestResults.FailedDueToOverload;
                    case "NO":
                        return SelfTestResults.NoResultsAvailable;
                    default:
                        return SelfTestResults.Unknown;
                }
            }
        }

        public decimal BatteryLevel
        {
            get { return GetDecimal("f"); }
        }

        public int EstimatedRuntime
        {
            get
            {
                var answer = GetString("j");
                answer = answer.TrimEnd(':');
                int result;
                return Int32.TryParse(answer, NumberStyles.Number, _numberFormatInfo, out result) ? result : 0;
            }
        }

        public LineQualities LineQuality
        {
            get
            {
                var r = GetString("9");
                switch (r)
                {
                    case "00":
                        return LineQualities.Unacceptable;
                    case "FF":
                        return LineQualities.Acceptable;
                    default:
                        return LineQualities.Unknown;
                }
            }
        }

        #endregion

        #region Перечисления

        #region AlarmDelays enum

        public enum AlarmDelays
        {
            Seconds5,
            Seconds30,
            LowBatteryOnly,
            Disabled,
            Unknown
        }

        #endregion

        #region AutomaticSelftestIntervals enum

        public enum AutomaticSelftestIntervals
        {
            Days14,
            Days7,
            On,
            Off,
            Unknown
        }

        #endregion

        #region CausesOfTransfer enum

        public enum CausesOfTransfer
        {
            UnacceptableUtilityVoltageRateOfChange,
            HighUtilityVoltage,
            LowUtilityVoltage,
            LineVoltageNotchOrSpike,
            NoTransfersYetSinceTurnon,
            TransferDueToUcommandOrActivationOfUpsTest,
            Unknown
        }

        #endregion

        #region LineQualities enum

        public enum LineQualities
        {
            Unacceptable,
            Acceptable,
            Unknown
        }

        #endregion

        #region Register1Flags enum

        [Flags]
        public enum Register1Flags
        {
            InWakeupMode = 0x01,
            InBypassModeDueToInternalFault = 0x02,
            GoingToBypassModeDueToCommand = 0x04,
            InBypassModeDueToCommand = 0x08,
            ReturningFromBypassMode = 0x10,
            InBypassModeDueToManualBypassControl = 0x20,
            ReadyToPowerLoadOnUserCommand = 0x40,
            ReadyToPowerLoadOnUserCommandOrReturnOfLinePower = 0x80
        }

        #endregion

        #region Register2Flags enum

        [Flags]
        public enum Register2Flags
        {
            FanFailureInElectronics = 0x01,
            FanFailureInIsolationUnit = 0x02,
            BypassSupplyFailure = 0x04,
            OutputVoltageSelectFailure = 0x08,
            DcImbalance = 0x10,
            CommandSentToStopBypassWithNoBatteryConnected = 0x20,
            RelayFaultInSmartTrimOrSmartBoost = 0x40,
            BadOutputVoltage = 0x80
        }

        #endregion

        #region Register3Flags enum

        [Flags]
        public enum Register3Flags
        {
            OutputUnpoweredDueToShutdownByLowBattery = 0x01,
            UnableToTransferToBatteryDueToOverload = 0x02,
            MainRelayMalfunction = 0x04,
            OnSleepMode = 0x08,
            InShutdownModeFrom = 0x10,
            BatteryChargerFailure = 0x20,
            BypassRelayMalfunction = 0x40,
            NormalOperatingTemperatureExceeded = 0x80
        }

        #endregion

        #region ReturnThresholds enum

        public enum ReturnThresholds
        {
            P00 = 0,
            P15 = 1,
            P25 = 2,
            P90 = 3
        }

        #endregion

        #region SelfTestResults enum

        public enum SelfTestResults
        {
            GoodBattery,
            FailedDueToInsufficientCapacity,
            FailedDueToOverload,
            NoResultsAvailable,
            Unknown
        }

        #endregion

        #region Sensitivities enum

        public enum Sensitivities
        {
            Highest,
            Medium,
            Lowest,
            Autoadjust,
            Unknown
        }

        #endregion

        #region StatusFlags enum

        [Flags]
        public enum StatusFlags
        {
            RuntimeCalibrationOccurring = 0x01,
            SmartTrim = 0x02,
            SmartBoost = 0x04,
            OnLine = 0x08,
            OnBattery = 0x10,
            OverloadedOutput = 0x20,
            BatteryLow = 0x40,
            ReplaceBattery = 0x80
        }

        #endregion

        #endregion
    }
}